﻿/**
 * Copyright (c) 2013 Nokia Corporation.
 */

using System;
using System.Collections.Generic;
using System.Device.Location;
using System.Diagnostics;
using System.Linq;
using System.Net;
using System.Text;
using System.Windows;
using System.Windows.Threading;
using TweetSharp;

namespace Dradis
{
    /// <summary>
    /// Wraps the Twitter functions used by the app. This class utilizes the
    /// Tweetsharp package which can be installed via Nuget package manager.
    /// For more information about Tweetsharp, see
    /// https://github.com/danielcrenna/tweetsharp
    /// </summary>
    class TwitterManager
    {
        // Constants
        private const int BadAuthenticationDataErrorCode = 215;
        private const String OAuthVerifierString = "oauth_verifier";
        public static char HashtagChar = '#';

        // Twitter API keys
        private const String ConsumerKey = "";
        private const String ConsumerSecret = "";

        // Members
        private static TwitterManager _instance = null;
        private ApplicationSettings _settings = null;
        
        private TwitterService _service = null;
        public TwitterService Service
        {
            get
            {
                return _service;
            }
        }

        private OAuthRequestToken _requestToken = null;
        public OAuthRequestToken RequestToken
        {
            get
            {
                return _requestToken;
            }
            set
            {
                _requestToken = value;
            }
        }
        
        private bool _userAuthenticated = false;
        public bool IsAuthenticated
        {
            get
            {
                return _userAuthenticated;
            }
            set
            {
                _userAuthenticated = value;
            }
        }

        public event EventHandler<Uri> LoadAuthWebPage;
        public event EventHandler<bool> AuthenticationResult;
        public event EventHandler<String> TweetingResult;

        /// <summary>
        /// Returns the singleton instance of this class.
        /// </summary>
        /// <returns>The singleton instance of this class.</returns>
        public static TwitterManager GetInstance()
        {
            if (_instance == null)
            {
                _instance = new TwitterManager();
            }

            return _instance;
        }

        /// <summary>
        /// Constructor.
        /// </summary>
        private TwitterManager()
        {
            if (ConsumerKey.Length == 0 || ConsumerSecret.Length == 0)
            {
                // Get the API keys for Twitter at https://dev.twitter.com/apps
                throw new ArgumentException("No Twitter consumer key or secret available!");
            }
            
            _service = new TwitterService(ConsumerKey, ConsumerSecret);
            _settings = ApplicationSettings.GetInstance();

            MessageProtocol.Init();

            OAuthAccessToken existingToken = _settings.TwitterToken;

            if (existingToken != null)
            {
                // Try to authenticate using the existing token. Note that
                // settings have to be loaded before this can work.
                _service.AuthenticateWith(existingToken.Token,
                                          existingToken.TokenSecret);
                _userAuthenticated = true;
            }
        }

        /// <summary>
        /// Starts the authentication process.
        /// </summary>
        public void Authenticate()
        {
            _service.GetRequestToken(ProcessRequestToken);
        }

        /// <summary>
        /// 
        /// </summary>
        /// <param name="uri"></param>
        /// <returns></returns>
        public bool GetAccessTokenFromUri(String uri)
        {
            if (uri.Contains(TwitterManager.OAuthVerifierString))
            {
                var values = ParseQueryString(uri);
                String verifier = values[TwitterManager.OAuthVerifierString];
                _service.GetAccessToken(_requestToken, verifier, ProcessAccessToken);
                return true;
            }

            return false;
        }

        /// <summary>
        /// Sends a tweet containing the given message and location if
        /// provided.
        /// </summary>
        /// <param name="content">The tweet message content.</param>
        /// <param name="coordinate">The user location (optional).</param>
        public void SendTweet(String content, GeoCoordinate coordinate)
        {
            if (_userAuthenticated)
            {
                SendTweetOptions options = new SendTweetOptions();
                options.Status = content;

                if (coordinate == null)
                {
                    options.DisplayCoordinates = false;
                }
                else
                {
                    options.Lat = coordinate.Latitude;
                    options.Long = coordinate.Longitude;
                    options.DisplayCoordinates = true;
                }

                _service.SendTweet(options, OnTweetResponse);
            }
        }

        /// <summary>
        /// Processes the request token.
        /// </summary>
        /// <param name="token"></param>
        /// <param name="response"></param>
        private void ProcessRequestToken(OAuthRequestToken token, TwitterResponse response)
        {
            if (token != null)
            {
                _requestToken = token;
                EventHandler<Uri> handler = LoadAuthWebPage;

                if (handler != null)
                {
                    handler(this, _service.GetAuthorizationUri(_requestToken));
                }
            }
        }

        /// <summary>
        /// Processes the access token. If valid, the token is stored.
        /// </summary>
        /// <param name="token"></param>
        /// <param name="response"></param>
        private void ProcessAccessToken(OAuthAccessToken token, TwitterResponse response)
        {
            EventHandler<bool> handler = AuthenticationResult;

            if (token != null)
            {
                _service.AuthenticateWith(token.Token, token.TokenSecret);

                // Save the token
                _settings.TwitterToken = token;
                _settings.Save();

                _userAuthenticated = true;

                if (handler != null)
                {
                    handler(this, true);
                }
            }
            else if (handler != null)
            {
                // Failed to authenticate
                handler(this, false);
            }
        }

        /// <summary>
        /// Clears the authorization data and notifies the listeners. This
        /// should be called when the token has been expired.
        /// </summary>
        private void ClearAuthorizationData()
        {
            _userAuthenticated = false;
            _settings.TwitterToken = null;
            _settings.Save();

            EventHandler<bool> authHandler = AuthenticationResult;

            if (authHandler != null)
            {
                authHandler(this, false);
            }
        }

        /// <summary>
        /// Called when the result of tweet sending is known.
        /// </summary>
        /// <param name="tweet"></param>
        /// <param name="response"></param>
        private void OnTweetResponse(TwitterStatus tweet, TwitterResponse response)
        {
            EventHandler<String> handler = TweetingResult;

            if (handler == null)
            {
                // There is no-one to notify
                return;
            }

            if (response.StatusCode == HttpStatusCode.OK)
            {
                handler(this, null);
            }
            else
            {
                Debug.WriteLine("TwitterManager::OnTweetResponse(): "
                    + ((response == null) ? "No response" : response.Error.Message));

                if (response.Error.Code == BadAuthenticationDataErrorCode
                    || response.StatusCode == HttpStatusCode.Unauthorized)
                {
                    Debug.WriteLine("TwitterManager::OnTweetResponse(): Bad authentication/unauthorized!");
                    ClearAuthorizationData();
                }

                handler(this, response.Error.Message);
            }
        }

        // From Hammock.Extensions.StringExtensions.cs
        private static IDictionary<string, string> ParseQueryString(string query)
        {
            // [DC]: This method does not URL decode, and cannot handle decoded input
            if (query.StartsWith("?")) query = query.Substring(1);
            if (query.Equals(string.Empty))
            {
                return new Dictionary<string, string>();
            }
            var parts = query.Split(new[] { '&' });
            return parts.Select(
                part => part.Split(new[] { '=' })).ToDictionary(
                    pair => pair[0], pair => pair[1]
                );
        }


        /**********************************************************************
         * Dradis app specific methods start here
         */

        public String LatestTweet { get; set; }
        public event EventHandler<User> LocationReceived;
        public event EventHandler<String> NewTweetMessageReceived;

        /// <summary>
        /// Sends the user location using the simple message protocol.
        /// </summary>
        /// <param name="coordinate">The user location.</param>
        public void SendLocation(GeoCoordinate coordinate)
        {
            OAuthAccessToken token = _settings.TwitterToken;

            if (token == null)
            {
                Debug.WriteLine("TwitterManager::SendLocation(): No token!");
                return;
            }

            RetrieveMyLatestTweetAndDelete();

            String message =
                MessageProtocol.CreateMessage(_settings.UserObjectType,
                                              coordinate,
                                              _settings.TwitterChannel);

            Debug.WriteLine("TwitterManager::SendLocation(): Will send: '"
                + message + "'");

            SendTweet(message, coordinate);
        }

        /// <summary>
        /// Searches for users (from tweets containing the set hashtag).
        /// </summary>
        public void SearchForUsers()
        {
            if (_settings.TwitterChannel == null
                || _settings.TwitterChannel.Length == 0)
            {
                Debug.WriteLine("TwitterManager::SearchForUsers(): Failed!");
                return;
            }

            SearchOptions options = new SearchOptions();
            options.Q = HashtagChar + _settings.TwitterChannel;
            _service.Search(options, OnTwitterSearchResult);
            Debug.WriteLine("TwitterManager::SearchForUsers(): Searching...");
        }

        /// <summary>
        /// Called when the search for users returns a result.
        /// </summary>
        /// <param name="result"></param>
        /// <param name="response"></param>
        private void OnTwitterSearchResult(TwitterSearchResult result, TwitterResponse response)
        {
            if (response.StatusCode == HttpStatusCode.OK)
            {
                List<TwitterStatus> statuses = new List<TwitterStatus>(result.Statuses);
                bool latest = true;

                foreach (TwitterStatus status in statuses)
                {
                    Deployment.Current.Dispatcher.BeginInvoke(() =>
                    {
                        EventHandler<User> locationReceivedHandler = LocationReceived;
                        User user = CreateNewUser(status);

                        if (user.Location != null && locationReceivedHandler != null)
                        {
                            locationReceivedHandler(this, user);
                        }

                        if (latest)
                        {
                            if (LatestTweet == null
                                || (statuses[0].Text != null
                                && !LatestTweet.Equals(statuses[0].Text)))
                            {
                                EventHandler<String> newTweetReceivedHandler = NewTweetMessageReceived;
                                LatestTweet = statuses[0].Text;

                                if (newTweetReceivedHandler != null)
                                {
                                    newTweetReceivedHandler(this, "[" + statuses[0].User.ScreenName + "] " + LatestTweet);
                                }

                                latest = false;
                            }
                        }
                    });
                }
            }
            else if (response.Error != null)
            {
                Debug.WriteLine("TwitterManager::OnTwitterSearchResult(): "
                    + ((response == null) ? "No response" : response.Error.Message));

                if (response.Error.Code == BadAuthenticationDataErrorCode
                    || response.StatusCode == HttpStatusCode.Unauthorized)
                {
                    ClearAuthorizationData();
                }
            }
        }

        /// <summary>
        /// Tries to find the user's latest location tweet and tries to
        /// delete one if found.
        /// </summary>
        private void RetrieveMyLatestTweetAndDelete()
        {
            SearchOptions options = new SearchOptions();
            options.Q = HashtagChar + _settings.TwitterChannel;
            _service.Search(options, DeleteMyLatestTweetIfFound);
        }

        /// <summary>
        /// Deletes the user's latest location tweet from the found tweets.
        /// </summary>
        /// <param name="result"></param>
        /// <param name="response"></param>
        private void DeleteMyLatestTweetIfFound(TwitterSearchResult result, TwitterResponse response)
        {
            Debug.WriteLine("TwitterManager::DeleteMyLatestTweetIfFound(): " + response.StatusCode);

            if (response.StatusCode != HttpStatusCode.OK)
            {
                return;
            }

            List<TwitterStatus> statuses = new List<TwitterStatus>(result.Statuses);

            foreach (TwitterStatus status in statuses)
            {
                if (status == null
                    || status.User == null
                    || !status.User.ScreenName.Equals(_settings.UserName))
                {
                    continue;
                }

                if (MessageProtocol.MatchesProtocol(status.Text))
                {
                    // Delete this
                    Debug.WriteLine("TwitterManager::DeleteMyLatestTweetIfFound(): Found a tweet to delete!");
                    DeleteTweetOptions options = new DeleteTweetOptions();
                    options.Id = status.Id;
                    _service.DeleteTweet(options, OnMyLatestTweetDeleted);
                }
            }
        }

        /// <summary>
        /// Gets called when the attempt to delete a user's tweet returns a
        /// result.
        /// </summary>
        /// <param name="status"></param>
        /// <param name="response"></param>
        private void OnMyLatestTweetDeleted(TwitterStatus status, TwitterResponse response)
        {
            Debug.WriteLine("TwitterManager::OnMyLatestTweetDeleted(): " + response.StatusCode);
        }

        /// <summary>
        /// Creates a new User instance based on the details of the given
        /// Twitter status.
        /// </summary>
        /// <param name="status">A Twitter status.</param>
        /// <returns>A newly created user.</returns>
        private User CreateNewUser(TwitterStatus status)
        {
            User user = new User();
            user.UserName = status.User.ScreenName;

            if (status.Location != null)
            {
                user.Location = new GeoCoordinate(
                    status.Location.Coordinates.Latitude,
                    status.Location.Coordinates.Longitude);
            }
            else
            {
                Debug.WriteLine("TwitterManager::OnTwitterSearchResult(): Tweet metadata contains no location for '"
                    + user.UserName + "'");
            }

            if (status.Text != null)
            {
                Debug.WriteLine("TwitterManager::OnTwitterSearchResult(): " + status.Text);
                MessageProtocol.ParseMessage(status.Text, user);
            }

            return user;
        }
    }
}
